@page "/user/login"
@inherits AliasVault.Client.Auth.Pages.Base.LoginBase
@layout Auth.Layout.MainLayout
@inject Config Config
@attribute [AllowAnonymous]
@using System.Text.Json
@using AliasVault.Shared.Models.WebApi.Auth
@using AliasVault.Client.Auth.Components
@using AliasVault.Client.Auth.Models
@using AliasVault.Client.Utilities
@using AliasVault.Cryptography.Client
@using SecureRemotePassword
@using Microsoft.Extensions.Localization

@if (_showTwoFactorAuthStep)
{
    <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
        @Localizer["TwoFactorAuthenticationTitle"]
    </h2>

    <ServerValidationErrors @ref="_serverValidationErrors" />

    <p class="text-gray-700 dark:text-gray-300 mb-6">@Localizer["TwoFactorAuthenticationDescription"]</p>
    <div class="w-full">
        <EditForm Model="_loginModel2Fa" FormName="login-with-2fa" OnValidSubmit="Handle2Fa" method="post" class="space-y-6">
            <DataAnnotationsValidator/>
            <div>
                <label for="two-factor-code" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["AuthenticatorCodeLabel"]</label>
                <InputNumber @bind-Value="_loginModel2Fa.TwoFactorCode"
                            id="two-factor-code"
                            @oninput="OnTwoFactorCodeInput"
                            class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                            autocomplete="off"/>
                <ValidationMessage For="() => _loginModel2Fa.TwoFactorCode" class="text-red-600 dark:text-red-400 text-sm mt-1"/>
            </div>
            <div class="flex items-start">
                <div class="flex items-center h-5">
                    <InputCheckbox @bind-Value="_loginModel2Fa.RememberMachine" id="remember-machine" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800"/>
                </div>
                <div class="ml-3 text-sm">
                    <label for="remember-machine" class="font-medium text-gray-900 dark:text-white">@Localizer["RememberMachineLabel"]</label>
                </div>
            </div>
            <button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">@Localizer["LoginButton"]</button>
        </EditForm>
    </div>
    <p class="mt-6 text-sm text-gray-700 dark:text-gray-300">
        @Localizer["DontHaveAuthenticatorText"]
    </p>
    <p class="text-sm text-gray-700 dark:text-gray-300">
        <button @onclick="LoginWithRecoveryCode" class="text-primary-600 hover:underline dark:text-primary-500">@Localizer["LoginWithRecoveryCodeLink"]</button>
    </p>
}
else if (_showLoginWithRecoveryCodeStep)
{
    <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
        @Localizer["RecoveryCodeVerificationTitle"]
    </h2>

    <ServerValidationErrors @ref="_serverValidationErrors" />

    <p class="text-gray-700 dark:text-gray-300 mb-6">
        @Localizer["RecoveryCodeDescription"]
    </p>
    <div class="w-full">
        <EditForm Model="_loginModelRecoveryCode" FormName="login-with-recovery-code" OnValidSubmit="HandleRecoveryCode" method="post" class="space-y-6">
            <DataAnnotationsValidator/>
            <div>
                <label for="two-factor-code" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["RecoveryCodeLabel"]</label>
                <InputText @bind-Value="_loginModelRecoveryCode.RecoveryCode" id="recovery-code" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" autocomplete="off"/>
                <ValidationMessage For="() => _loginModelRecoveryCode.RecoveryCode" class="text-red-600 dark:text-red-400 text-sm mt-1"/>
            </div>
            <button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">@Localizer["LoginButton"]</button>
        </EditForm>
    </div>
    <p class="mt-6 text-sm text-gray-700 dark:text-gray-300">
        @Localizer["RegainedAccessText"]
    </p>
    <p class="text-sm text-gray-700 dark:text-gray-300">
        <button @onclick="LoginWithAuthenticator" class="text-primary-600 hover:underline dark:text-primary-500">@Localizer["LoginWithAuthenticatorLink"]</button>
    </p>
}
else
{
    <h2 class="text-2xl font-bold text-gray-900 dark:text-white">
        @Localizer["PageTitle"]
    </h2>

    <FullScreenLoadingIndicator @ref="_loadingIndicator"/>

    <EditForm Model="_loginModel" OnValidSubmit="HandleLogin" class="mt-4 space-y-6">
        <ServerValidationErrors @ref="_serverValidationErrors"/>
        <DataAnnotationsValidator/>
        <div>
            <label asp-for="Input.Email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["UsernameOrEmailLabel"]</label>
            <InputTextField id="email" @bind-Value="_loginModel.Username" type="text" placeholder="@Localizer["UsernamePlaceholder"]" autocapitalize="off" autocorrect="off"/>
            <ValidationMessage For="() => _loginModel.Username"/>
        </div>
        <div>
            <label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["PasswordLabel"]</label>
            <PasswordInputField id="password" @bind-Value="_loginModel.Password" placeholder="@Localizer["PasswordPlaceholder"]"/>
            <ValidationMessage For="() => _loginModel.Password"/>
        </div>

        <div class="flex items-start">
            <div class="flex items-center h-5">
                <InputCheckbox @bind-Value="_loginModel.RememberMe" id="remember" class="w-4 h-4 border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:focus:ring-primary-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600" />
            </div>
            <div class="ml-3 text-sm">
                <label for="remember" class="font-medium text-gray-900 dark:text-white">@Localizer["RememberMeLabel"]</label>
            </div>
            <a href="/user/forgot-password" class="ml-auto text-sm text-primary-700 hover:underline dark:text-primary-500">@Localizer["LostPasswordLink"]</a>
        </div>

        <div class="flex flex-col gap-4">
            <button type="submit" id="login-button" class="w-full px-5 py-2 text-base font-medium text-center text-white bg-primary-700 rounded-lg hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800 flex items-center justify-center gap-2">
                @Localizer["LoginButton"]
            </button>
            <button type="button" id="mobile-login-button" @onclick="() => _showMobileLoginModal = true" class="hidden md:flex w-full px-5 py-2 text-base font-medium text-center text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:hover:bg-gray-600 dark:focus:ring-gray-700 items-center justify-center gap-2">
                <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
                </svg>
                @Localizer["MobileDeviceLink"]
            </button>
        </div>


        @if (Config.PublicRegistrationEnabled)
        {
            <div class="text-sm font-medium text-gray-500 dark:text-gray-400 text-center">
                @Localizer["NoAccountYetText"] <a href="/user/setup" class="text-primary-700 hover:underline dark:text-primary-500">@Localizer["CreateNewVaultLink"]</a>
            </div>
        }
    </EditForm>
}

<FooterLogin />

<MobileUnlockModal IsOpen="@_showMobileLoginModal"
                   OnClose="() => _showMobileLoginModal = false"
                   OnSuccess="HandleMobileLoginSuccess"
                   Mode="login" />

@code {
    private readonly LoginFormModel _loginModel = new();
    private readonly LoginModel2Fa _loginModel2Fa = new();
    private readonly LoginModelRecoveryCode _loginModelRecoveryCode = new();
    private FullScreenLoadingIndicator _loadingIndicator = new();
    private ServerValidationErrors _serverValidationErrors = new();
    private bool _showTwoFactorAuthStep;
    private bool _showLoginWithRecoveryCodeStep;
    private bool _showMobileLoginModal;

    private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Auth.Login", "AliasVault.Client");
    private IStringLocalizer ApiErrorLocalizer => LocalizerFactory.Create("ApiErrors", "AliasVault.Client");
    private IStringLocalizer SharedLocalizer => LocalizerFactory.Create("SharedResources", "AliasVault.Client");

    private SrpEphemeral _clientEphemeral = new();
    private SrpSession _clientSession = new();
    private byte[] _passwordHash = [];

    /// <inheritdoc />
    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();

        await AuthStateProvider.GetAuthenticationStateAsync();
        var authState = await AuthStateProvider.GetAuthenticationStateAsync();
        if (authState.User.Identity?.IsAuthenticated == true) {
            // Already authenticated, redirect to home page.
            NavigationManager.NavigateTo("/");
        }
    }

    /// <inheritdoc />
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await Task.Delay(300); // Give time for the DOM to update
            await JsInteropService.FocusElementById("email");
        }
    }

    private async Task LoginWithAuthenticator()
    {
        _showLoginWithRecoveryCodeStep = false;
        _showTwoFactorAuthStep = true;
        StateHasChanged();

        await Task.Delay(100); // Give time for the DOM to update
        await JsInteropService.FocusElementById("two-factor-code");
    }

    private void LoginWithRecoveryCode()
    {
        _showLoginWithRecoveryCodeStep = true;
        _showTwoFactorAuthStep = false;
        StateHasChanged();
    }

    /// <summary>
    /// Handle the basic login request with username and password.
    /// </summary>
    private async Task HandleLogin()
    {
        _loadingIndicator.Show(Localizer["LoggingInMessage"]);
        _serverValidationErrors.Clear();

        try
        {
            var errors = await ProcessLoginAsync();
            foreach (var error in errors)
            {
                _serverValidationErrors.AddError(error);
            }
        }
#if DEBUG
    catch (Exception ex)
        {
        // If in debug mode show the actual exception.
        _serverValidationErrors.AddError(ex.ToString());
        }
#else
        catch
        {
            // If in release mode show a generic error.
            _serverValidationErrors.AddError(Localizer["LoginErrorMessage"]);
        }
#endif
        finally
        {
            _loadingIndicator.Hide();
        }
    }

    /// <summary>
    /// Process the login request using username and password.
    /// </summary>
    /// <returns>List of errors if something went wrong.</returns>
    private async Task<List<string>> ProcessLoginAsync()
    {
        GlobalNotificationService.ClearMessages();

        // Sanitize username
        var username = _loginModel.Username.ToLowerInvariant().Trim();

        // Send request to server with username to get server ephemeral public key.
        var result = await Http.PostAsJsonAsync("v1/Auth/login", new LoginInitiateRequest(username));
        var responseContent = await result.Content.ReadAsStringAsync();

        if (!result.IsSuccessStatusCode)
        {
            return ApiResponseUtility.ParseErrorResponse(responseContent, ApiErrorLocalizer);
        }

        var loginResponse = JsonSerializer.Deserialize<LoginInitiateResponse>(responseContent);
        if (loginResponse == null)
        {
            return
            [
                Localizer["LoginRequestErrorMessage"],
        ];
        }

        // 3. Client derives shared session key.
        _passwordHash = await Encryption.DeriveKeyFromPasswordAsync(_loginModel.Password, loginResponse.Salt, loginResponse.EncryptionType, loginResponse.EncryptionSettings);
        var passwordHashString = BitConverter.ToString(_passwordHash).Replace("-", string.Empty);

        _clientEphemeral = Srp.GenerateEphemeralClient();
        var privateKey = Srp.DerivePrivateKey(loginResponse.Salt, username, passwordHashString);
        _clientSession = Srp.DeriveSessionClient(
            privateKey,
            _clientEphemeral.Secret,
            loginResponse.ServerEphemeral,
            loginResponse.Salt,
            username);

        // 4. Client sends proof of session key to server.
        result = await Http.PostAsJsonAsync("v1/Auth/validate", new ValidateLoginRequest(username, _loginModel.RememberMe, _clientEphemeral.Public, _clientSession.Proof));
        responseContent = await result.Content.ReadAsStringAsync();

        if (!result.IsSuccessStatusCode)
        {
            return ApiResponseUtility.ParseErrorResponse(responseContent, ApiErrorLocalizer);
        }

        var validateLoginResponse = JsonSerializer.Deserialize<ValidateLoginResponse>(responseContent);
        if (validateLoginResponse == null)
        {
            return
            [
                Localizer["LoginRequestErrorMessage"],
        ];
        }

        // Check if 2FA is required, if yes, show 2FA step.
        if (validateLoginResponse.RequiresTwoFactor)
        {
            await LoginWithAuthenticator();
            return [];
        }

        // If no 2FA is required, verify the login.
        return await ProcessLoginVerify(validateLoginResponse);
    }

    /// <summary>
    /// Confirm 2-factor authentication protected login with recovery code.
    /// </summary>
    private async Task HandleRecoveryCode()
    {
        _loadingIndicator.Show(Localizer["VerifyingRecoveryCodeMessage"]);
        _serverValidationErrors.Clear();

        try
        {
            // Sanitize username
            var username = _loginModel.Username.ToLowerInvariant().Trim();

            // Validate 2-factor auth code auth and login
            var result = await Http.PostAsJsonAsync("v1/Auth/validate-recovery-code", new ValidateLoginRequestRecoveryCode(username, _loginModel.RememberMe, _clientEphemeral.Public, _clientSession.Proof, _loginModelRecoveryCode.RecoveryCode));
            var responseContent = await result.Content.ReadAsStringAsync();

            if (!result.IsSuccessStatusCode)
            {
                foreach (var error in ApiResponseUtility.ParseErrorResponse(responseContent, ApiErrorLocalizer))
                {
                    _serverValidationErrors.AddError(error);
                }
                return;
            }

            var validateLoginResponse = JsonSerializer.Deserialize<ValidateLoginResponse>(responseContent);
            if (validateLoginResponse == null)
            {
                _serverValidationErrors.AddError(Localizer["LoginRequestErrorMessage"]);
                return;
            }

            var errors = await ProcessLoginVerify(validateLoginResponse);
            foreach (var error in errors)
            {
                _serverValidationErrors.AddError(error);
            }
        }
#if DEBUG
        catch (Exception ex)
        {
            // If in debug mode show the actual exception.
            _serverValidationErrors.AddError(ex.ToString());
        }
#else
        catch
        {
            // If in release mode show a generic error.
            _serverValidationErrors.AddError(Localizer["LoginErrorMessage"]);
        }
#endif
        finally
        {
            _loadingIndicator.Hide();
        }
    }

    /// <summary>
    /// Confirm 2-factor authentication protected login with authenticator code.
    /// </summary>
    private async Task Handle2Fa()
    {
        _loadingIndicator.Show(Localizer["VerifyingTwoFactorCodeMessage"]);
        _serverValidationErrors.Clear();

        try
        {
            // Sanitize username
            var username = _loginModel.Username.ToLowerInvariant().Trim();

            // Validate 2-factor auth code auth and login
            var result = await Http.PostAsJsonAsync("v1/Auth/validate-2fa", new ValidateLoginRequest2Fa(username, _loginModel.RememberMe, _clientEphemeral.Public, _clientSession.Proof, _loginModel2Fa.TwoFactorCode ?? 0));
            var responseContent = await result.Content.ReadAsStringAsync();

            if (!result.IsSuccessStatusCode)
            {
                foreach (var error in ApiResponseUtility.ParseErrorResponse(responseContent, ApiErrorLocalizer))
                {
                    _serverValidationErrors.AddError(error);
                }
                return;
            }

            var validateLoginResponse = JsonSerializer.Deserialize<ValidateLoginResponse>(responseContent);
            if (validateLoginResponse == null)
            {
                _serverValidationErrors.AddError(Localizer["LoginRequestErrorMessage"]);
                return;
            }

            var errors = await ProcessLoginVerify(validateLoginResponse);
            foreach (var error in errors)
            {
                _serverValidationErrors.AddError(error);
            }
        }
#if DEBUG
        catch (Exception ex)
        {
            // If in debug mode show the actual exception.
            _serverValidationErrors.AddError(ex.ToString());
        }
#else
        catch
        {
            // If in release mode show a generic error.
            _serverValidationErrors.AddError(Localizer["LoginErrorMessage"]);
        }
#endif
        finally
        {
            _loadingIndicator.Hide();
        }
    }

    /// <summary>
    /// Verify the login response and store the tokens.
    /// </summary>
    private async Task<List<string>> ProcessLoginVerify(ValidateLoginResponse validateLoginResponse)
    {
        // 5. Client verifies proof.
        Srp.VerifySession(_clientEphemeral.Public, _clientSession, validateLoginResponse.ServerSessionProof);

        // Store the tokens in local storage.
        await AuthService.StoreAccessTokenAsync(validateLoginResponse.Token!.Token);
        await AuthService.StoreRefreshTokenAsync(validateLoginResponse.Token!.RefreshToken);

        // Store the encryption key in memory.
        await AuthService.StoreEncryptionKeyAsync(_passwordHash);

        await AuthStateProvider.GetAuthenticationStateAsync();
        GlobalNotificationService.ClearMessages();

        // Redirect to the page the user was trying to access before if set.
        var localStorageReturnUrl = await LocalStorage.GetItemAsync<string>(ReturnUrlKey);
        if (!string.IsNullOrEmpty(localStorageReturnUrl))
        {
            await LocalStorage.RemoveItemAsync(ReturnUrlKey);
            NavigationManager.NavigateTo(localStorageReturnUrl);
        }
        else
        {
            NavigationManager.NavigateTo("/");
        }

        return [];
    }

    /// <summary>
    /// Auto submit the 2FA code when 6 digits are entered.
    /// </summary>
    /// <param name="e"></param>
    private async Task OnTwoFactorCodeInput(ChangeEventArgs e)
    {
        if (e.Value?.ToString()?.Length >= 6)
        {
            // Update the blazor model with the current value.
            _loginModel2Fa.TwoFactorCode = int.Parse(e.Value.ToString()!);

            // Submit the form.
            await Handle2Fa();
        }
        else
        {
            _serverValidationErrors.Clear();
        }
    }

    /// <summary>
    /// Handle successful mobile login.
    /// </summary>
    private async Task HandleMobileLoginSuccess(MobileLoginResult result)
    {
        _loadingIndicator.Show(Localizer["LoggingInMessage"]);
        _serverValidationErrors.Clear();

        try
        {
            // Clear global messages
            GlobalNotificationService.ClearMessages();

            // Call /login endpoint to retrieve salt and encryption settings
            var loginInitiateRequest = new { username = result.Username };
            var loginResponse = await Http.PostAsJsonAsync("v1/Auth/login", loginInitiateRequest);

            if (!loginResponse.IsSuccessStatusCode)
            {
                _serverValidationErrors.AddError(Localizer["LoginErrorMessage"]);
                return;
            }

            var loginData = await loginResponse.Content.ReadFromJsonAsync<LoginInitiateResponse>();
            if (loginData == null)
            {
                _serverValidationErrors.AddError(Localizer["LoginErrorMessage"]);
                return;
            }

            // Store the tokens in local storage
            await AuthService.StoreAccessTokenAsync(result.Token);
            await AuthService.StoreRefreshTokenAsync(result.RefreshToken);

            // Convert decryption key from base64 string to byte array
            var decryptionKeyBytes = Convert.FromBase64String(result.DecryptionKey);

            // Store the encryption key in memory
            await AuthService.StoreEncryptionKeyAsync(decryptionKeyBytes);

            await AuthStateProvider.GetAuthenticationStateAsync();
            GlobalNotificationService.ClearMessages();

            // Redirect to the page the user was trying to access before if set
            var localStorageReturnUrl = await LocalStorage.GetItemAsync<string>(ReturnUrlKey);
            if (!string.IsNullOrEmpty(localStorageReturnUrl))
            {
                await LocalStorage.RemoveItemAsync(ReturnUrlKey);
                NavigationManager.NavigateTo(localStorageReturnUrl);
            }
            else
            {
                NavigationManager.NavigateTo("/");
            }
        }
#if DEBUG
        catch (Exception ex)
        {
            // If in debug mode show the actual exception.
            _serverValidationErrors.AddError(ex.ToString());
        }
#else
        catch
        {
            // If in release mode show a generic error.
            _serverValidationErrors.AddError(Localizer["LoginErrorMessage"]);
        }
#endif
        finally
        {
            _loadingIndicator.Hide();
        }
    }

}
